package com.cltech.frame.query;

import com.cltech.frame.transformer.AliasToEntityCamelMapResultTransformer;
import com.cltech.frame.transformer.BeanTransformerAdapter;
import com.cltech.frame.transformer.SmartTransformer;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.DynaProperty;
import org.apache.commons.beanutils.PropertyUtils;
import org.hibernate.query.Query;
import org.hibernate.transform.ResultTransformer;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.beans.PropertyDescriptor;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * QueryBuilder.
 * <p/>
 *
 * @author guanbo
 * @version 1.0.0.0
 */
public final class QueryBuilder {
    /**
     * 默认构造方法.
     */
    private QueryBuilder() {
    }

    /**
     * 排序正则表达式.
     */
    private static final Pattern ORDERBY_PATTERN = Pattern
            .compile("order\\s+by.+?$", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
    /**
     * 结果集转换缓存.
     */
    private static Map<Class, ResultTransformer> transformerCache = new ConcurrentHashMap<>();

    /**
     * transform.
     * @param query Query
     * @param clazz clazz
     * @param <C> 类型
     * @return 结果集
     */
    public static <C> Query transform(final Query query, final Class<C> clazz) {
        ResultTransformer transformer;
        if (Map.class.isAssignableFrom(clazz)) {
            transformer = AliasToEntityCamelMapResultTransformer.INSTANCE;
        } else if (Number.class.isAssignableFrom(clazz) || clazz.isPrimitive()
                || String.class.isAssignableFrom(clazz) || Date.class.isAssignableFrom(clazz)) {
            transformer = transformerCache.computeIfAbsent(clazz, SmartTransformer::new);
        } else {
            transformer = transformerCache.computeIfAbsent(clazz, BeanTransformerAdapter::new);
        }
        return query.setResultTransformer(transformer);
    }


    /**
     * wrapCountQuery.
     * @param query query
     * @return wrapCountQuery
     */
    private static String wrapCountQuery(final String query) {
        return "select count(*) from (" + query + ") as ctmp";
    }

    /**
     * cleanOrderBy.
     * @param query  query
     * @return cleanOrderByQuery
     */
    private static String cleanOrderBy(final String query) {
        final Matcher matcher = ORDERBY_PATTERN.matcher(query);
        final StringBuffer sb = new StringBuffer();
        int i = 0;
        while (matcher.find()) {
            final String part = matcher.group(i);
            if (canClean(part)) {
                matcher.appendReplacement(sb, "");
            } else {
                matcher.appendReplacement(sb, part);
            }
            i++;
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

    /**
     * canCleanOrderBy.
     * @param orderByPart orderByPart
     * @return canClean
     */
    private static boolean canClean(final String orderByPart) {
        return orderByPart != null && (!orderByPart.contains(")")
                || StringUtils.countOccurrencesOf(orderByPart, ")") == StringUtils.countOccurrencesOf(orderByPart, "("));
    }

    /**
     * toCountQuery.
     * @param query query
     * @return countQuery
     */
    public static String toCountQuery(final String query) {
        return wrapCountQuery(cleanOrderBy(query));
    }

    /**
     * setParams.
     * @param query query
     * @param beanOrMap beanOrMap
     */
    public static void setParams(final Query query, final Object beanOrMap) {
        Set<String> pmds = query.getParameterMetadata().getNamedParameterNames();
        if (pmds != null) {
            final Map<String, Object> params = toParams(beanOrMap);
            for (String key : pmds) {
               final Object arg = params.get(key);
                if (arg == null) {
                    throw new InvalidDataAccessApiUsageException("For queries with named parameters you need to use provide names for method parameters. Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters.");
                } else if (arg.getClass().isArray()) {
                    query.setParameterList(key, (Object[]) arg);
                } else if (arg instanceof Collection) {
                    query.setParameterList(key, (Collection) arg);
                } else if (arg.getClass().isEnum()) {
                    query.setParameter(key, ((Enum) arg).ordinal());
                }else {
                    query.setParameter(key, arg);
                }
            }
        }
    }

    /**
     * toParams.
     * @param beanOrMap beanOrMap
     * @return Map
     */
    @SuppressWarnings("unchecked")
    public static Map<String, Object> toParams(final Object beanOrMap) {
        Map<String, Object> params;
        if (beanOrMap instanceof Map) {
            params = (Map<String, Object>) beanOrMap;
        } else {
            params = toMap(beanOrMap);
        }
        if (!CollectionUtils.isEmpty(params)) {
            final Iterator<String> keys = params.keySet().iterator();
            while (keys.hasNext()) {
                final String key = keys.next();
                if (!isValidValue(params.get(key))) {
                    keys.remove();
                }
            }
        }
        return params;
    }

    /**
     * isValidValue.
     * @param object object
     * @return boolean
     */
    public static boolean isValidValue(final Object object) {
        if (object == null) {
            return false;
        }
        return !(object instanceof Collection && CollectionUtils.isEmpty((Collection<?>) object));
    }

    /**
     * toMap.
     * @param bean bean
     * @return Map
     */
    public static Map<String, Object> toMap(final Object bean) {
        if (bean == null) {
            return Collections.emptyMap();
        }
        try {
            final Map<String, Object> description = new HashMap<String, Object>();
            if (bean instanceof DynaBean) {
                final DynaProperty[] descriptors = ((DynaBean) bean).getDynaClass().getDynaProperties();
                for (DynaProperty descriptor : descriptors) {
                    final String name = descriptor.getName();
                    description.put(name, BeanUtils.getProperty(bean, name));
                }
            } else {
                final PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(bean);
                for (PropertyDescriptor descriptor : descriptors) {
                    final String name = descriptor.getName();
                    if (PropertyUtils.getReadMethod(descriptor) != null) {
                        description.put(name, PropertyUtils.getNestedProperty(bean, name));
                    }
                }
            }
            return description;
        } catch (final Exception e) {
            return Collections.emptyMap();
        }
    }
}
